// Berkeley Open Infrastructure for Network Computing
// http://boinc.berkeley.edu
// Copyright (C) 2005 University of California
//
// This is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation;
// either version 2.1 of the License, or (at your option) any later version.
//
// This software is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// To view the GNU Lesser General Public License visit
// http://www.gnu.org/copyleft/lesser.html
// or write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

// Scheduler code for directing a client to one of several
// download servers based on its time zone

#include "config.h"
#include <string>
#include <stdio.h>

#include "parse.h"

#include "server_types.h"
#include "sched_msgs.h"

#ifdef _USING_FCGI_
#include "fcgi_stdio.h"
#else
#define FCGI_ToFILE(x) (x)
#endif

typedef struct urltag {
    int zone;
    char name[124];
} URLTYPE;


// these global variables are needed to pass information into the
// compare function below.
//
static int tzone=0;
static int hostid=0;

// Evaluate differences between time-zone.  Two time zones that differ
// by almost 24 hours are actually very close on the surface of the
// earth.  This function finds the 'shortest way around'
//
static int compare(const void *x, const void *y) {
    const URLTYPE *a=(const URLTYPE *)x;
    const URLTYPE *b=(const URLTYPE *)y;
    
    char longname[512];
    
    const int twelve_hours = 12*3600;

    int diffa = abs(tzone - (a->zone));
    int diffb = abs(tzone - (b->zone));

    if (diffa > twelve_hours) {
        diffa = 2*twelve_hours-diffa;
    }
    
    if (diffb > twelve_hours) {
        diffb = 2*twelve_hours-diffb;
    }

    if (diffa < diffb) {
        return -1;
    }
    
    if (diffa > diffb) {
        return +1;
    }
    
    // In order to ensure uniform distribution, we hash paths that are
    // equidistant from the host's timezone in a way that gives a
    // unique ordering for each host but which is effectively random
    // between hosts.
    //
    sprintf(longname, "%s%d", a->name, hostid);
    std::string sa = md5_string((const unsigned char *)longname, strlen((const char *)longname));
    sprintf(longname, "%s%d", b->name, hostid);
    std::string sb = md5_string((const unsigned char *)longname, strlen((const char *)longname));
    int xa = strtol(sa.substr(1, 7).c_str(), 0, 16);
    int xb = strtol(sb.substr(1, 7).c_str(), 0, 16);
    
    if (xa<xb) {
        return -1;
    }
    
    if (xa>xb) {
        return 1;
    }
    
    return 0;
}

static URLTYPE *cached=NULL;
#define BLOCKSIZE 32

URLTYPE* read_download_list() {
    FILE *fp;
    int count=0;
    int i;
    
    if (cached) return cached;
    
    if (!(fp=fopen("../download_servers", "r"))) {
        log_messages.printf(
            SCHED_MSG_LOG::MSG_CRITICAL,
            "File ../download_servers not found or unreadable!\n"
        );
        return NULL;
    }

    // read in lines from file
    while (1) {
        // allocate memory in blocks
        if ((count % BLOCKSIZE)==0) {
            cached=(URLTYPE *)realloc(cached, (count+BLOCKSIZE)*sizeof(URLTYPE));
            if (!cached) return NULL;
        }
        // read timezone offset and URL from file, and store in cache
        // list
        if (2==fscanf(FCGI_ToFILE(fp), "%d %s", &(cached[count].zone), cached[count].name)) {
            count++;
        } else {
            // provide a null terminator so we don't need to keep
            // another global variable for count.
            //
            cached[count].name[0]='\0';
            break;
        }
    }
    fclose(fp);
    
    if (!count) {
        log_messages.printf(
            SCHED_MSG_LOG::MSG_CRITICAL,
            "File ../download_servers contained no valid entries!\n"
            "Format of this file is one or more lines containing:\n"
            "TIMEZONE_OFFSET_IN_SEC   http://some.url.path\n"
        );
        free(cached);
        return NULL;
    }
    
    // sort URLs by distance from host timezone.  See compare() above
    // for details.
    qsort(cached, count, sizeof(URLTYPE), compare);
    
    log_messages.printf(
        SCHED_MSG_LOG::MSG_DEBUG,
        "Sorted list of URLs follows [host timezone: UTC%+d]\n",
        tzone
    );
    for (i=0; i<count; i++) {
        log_messages.printf(
            SCHED_MSG_LOG::MSG_DEBUG,
            "zone=%+06d url=%s\n", cached[i].zone, cached[i].name
        );
    }
    return cached;
}

// return number of bytes written, or <0 to indicate an error
//
int make_download_list(char *buffer, char *path, int tz) {
    char *start=buffer;
    int i;

    // global variable used in the compare() function
    tzone=tz;
    URLTYPE *serverlist=read_download_list();
    
    if (!serverlist) return -1;
    
    // print list of servers in sorted order.
    // Space is to format them nicely
    //
    for (i=0; strlen(serverlist[i].name); i++) {
        start+=sprintf(start, "%s<url>%s/%s</url>", i?"\n    ":"", serverlist[i].name, path);
    }

    // make a second copy in the same order
    //
    for (i=0; strlen(serverlist[i].name); i++) {
        start+=sprintf(start, "%s<url>%s/%s</url>", "\n    ", serverlist[i].name, path);
    }
    
    return (start-buffer);
}

// returns zero on success, non-zero to indicate an error
//
int add_download_servers(char *old_xml, char *new_xml, int tz) {
    char *p, *q, *r;
    
    p=r=old_xml;

    // search for next URL to do surgery on 
    while ((q=strstr(p, "<url>"))) {
        char *s;
        char path[1024];
        int len = q-p;
        
        strncpy(new_xml, p, len);
        
        new_xml += len;
        
        // locate next instance of </url>
        //
        if (!(r=strstr(q, "</url>"))) {
            return 1;
        }
        r += strlen("</url>");
        
        // parse out the URL
        //
        if (!parse_str(q, "<url>", path, 1024)) {
            return 1;
        }
        
        // find start of 'download/'
        //
        if (!(s=strstr(path,"download/"))) {
            return 1;
        }
        
        // insert new download list in place of the original one
        //
        len = make_download_list(new_xml, s, tz);
        if (len<0) {
            return 1;
        }
        new_xml += len;
        
        // advance pointer to start looking for next <url> tag.
        //
        p=r;
    }
    
    strcpy(new_xml, r);
    return 0;
}

// replace the download URL for apps with a list of
// multiple download servers.
//
void process_av_timezone(
    SCHEDULER_REPLY& reply, APP_VERSION* avp, APP_VERSION& av2
) {
    int retval;

    // set these global variables, needed by the compare()
    // function so that the download URL list can be sorted by timezone
    //
    tzone=reply.host.timezone;
    hostid=reply.host.id;
    retval = add_download_servers(avp->xml_doc, av2.xml_doc, reply.host.timezone);
    if (retval) {
        log_messages.printf(
            SCHED_MSG_LOG::MSG_CRITICAL,
            "add_download_servers(to APP version) failed\n"
        );
        // restore original WU!
        av2=*avp;
    }
}

// replace the download URL for WU files with a list of
// multiple download servers.
//
void process_wu_timezone(
    SCHEDULER_REPLY& reply, WORKUNIT& wu2, WORKUNIT& wu3
) {
    int retval;

    tzone=reply.host.timezone;
    hostid=reply.host.id;
        
    retval = add_download_servers(wu2.xml_doc, wu3.xml_doc, reply.host.timezone);
    if (retval) {
        log_messages.printf(
            SCHED_MSG_LOG::MSG_CRITICAL,
            "add_download_servers(to WU) failed\n"
        );
        // restore original WU!
        wu3=wu2;
    }
}

const char *BOINC_RCSID_28b6ac7093 = "$Id: sched_timezone.C,v 1.6 2005/11/21 18:34:35 korpela Exp $";

